"""Minimal SLIP helper for MicroPython streams.

``SlipStream`` wraps a UART-like object and moves complete SLIP frames—ideal
for shipping JSON strings—without blocking the caller. Feed it an existing
stream or pass ``uart_id`` to have it configure ``machine.UART`` for you. Call
``send`` with the bytes you want to transmit, drive ``poll`` from the main loop,
and fetch full frames with ``recv`` when they appear.
"""

import sys
from typing import Iterable, Optional, List

try:  # pragma: no cover - exercised by tests via monkeypatch
    from machine import UART as _MachineUART  # type: ignore
    from machine import Pin as _MachinePin  # type: ignore
except ImportError:  # pragma: no cover - CPython host path
    _MachineUART = None
    _MachinePin = None


def _ensure_pin(value):
    if _MachinePin is None:
        raise RuntimeError("machine.Pin is unavailable; Pin mode required")
    if isinstance(value, _MachinePin):
        return value
    return _MachinePin(value)


class SlipStream:
    """Wrap a UART-like object with SLIP framing.

    The class preserves whole messages—JSON strings, for example—while keeping
    reception non-blocking. ``send`` writes an encoded frame, ``poll`` advances
    the decoder when bytes are waiting, and ``recv`` hands back completed
    payloads. Oversized or malformed frames are dropped automatically.

    UART quick start
    ----------------
    >>> from machine import UART
    >>> from slip import SlipStream
    >>> uart = UART(0, baudrate=460800)
    >>> link = SlipStream(uart)
    >>> # ...or let the class build the UART for you (provide ``uart_id``)
    >>> link = SlipStream(uart_id=0, baudrate=460800)

    When auto-creating the port, additional UART arguments (like ``tx``/``rx``
    pins) can be supplied through ``uart_kwargs``. Pass ``stream`` to reuse an
    existing transport; omit it (or supply ``uart_id``) to let the class build
    ``machine.UART`` for you. By default the class behaves like an RP platform
    and wraps integer pin numbers into ``machine.Pin`` instances; specify
    ``platform_override`` (for example, ``"esp32"``) to keep your original pin
    values on other MCUs.
    >>> link.send(b'{"cmd":"ping"}')
    >>> link.poll(); link.recv()
    b'{"cmd":"pong"}'
    """

    END = 0xC0
    ESC = 0xDB
    ESC_END = 0xDC
    ESC_ESC = 0xDD

    def __init__(
        self,
        stream=None,
        max_frame_size: int = 1024,
        *,
        prefill_flusher: bool = True,
        uart_id: int = 1,
        baudrate: int = 460800,
        uart_bits: int = 8,
        uart_parity: Optional[int] = None,
        uart_stop: int = 1,
        uart_tx: int = 8,
        uart_rx: int = 9,
        uart_txbuf: int = 512,
        uart_rxbuf: int = 512,
        uart_kwargs: Optional[dict] = None,
        platform_override: Optional[str] = None,
    ) -> None:
        if stream is not None:
            if uart_kwargs:
                raise ValueError("Specify either stream or uart_id setup, not both")
            if uart_id not in (-1, 1):
                raise ValueError("Specify either stream or uart_id setup, not both")
        else:
            if uart_id == -1:
                raise ValueError("Provide uart_id to SlipStream when no stream is supplied")
            if _MachineUART is None:
                raise RuntimeError("machine.UART is unavailable in this environment")

            options = dict(uart_kwargs or {})
            options.setdefault("baudrate", baudrate)
            options.setdefault("bits", uart_bits)
            options.setdefault("parity", uart_parity)
            options.setdefault("stop", uart_stop)

            if platform_override is not None:
                platform_name = platform_override
            else:
                platform_name = sys.platform if sys.platform.startswith("rp") else "rp2"
            wrap_pins = platform_name.startswith("rp")

            if wrap_pins:
                tx_value = options.get("tx", uart_tx)
                rx_value = options.get("rx", uart_rx)
                options["tx"] = _ensure_pin(tx_value)
                options["rx"] = _ensure_pin(rx_value)
            else:
                options.setdefault("tx", uart_tx)
                options.setdefault("rx", uart_rx)

            options.setdefault("txbuf", uart_txbuf)
            options.setdefault("rxbuf", uart_rxbuf)

            stream = _MachineUART(uart_id, **options)

        if max_frame_size <= 0:
            raise ValueError("max_frame_size must be positive")

        self._stream = stream
        self._max_frame_size = max_frame_size
        self._prefill_flusher = prefill_flusher

        self._rx_buffer = bytearray()
        self._rx_frames = []  # type: List[bytes]
        self._escape_pending = False
        self._dropped_frames = 0

    # ------------------------------------------------------------------
    # Public TX API
    def send(self, payload: Iterable[int]) -> None:
        """SLIP-encode and transmit ``payload``.

        The method accepts any iterable yielding integers in ``0..255``. Strings
        are not allowed because SLIP operates on bytes.
        """

        encoded = bytearray()
        if self._prefill_flusher:
            encoded.append(self.END)

        for value in payload:
            if isinstance(value, str):
                raise TypeError("payload must be bytes-like, not str")
            byte = value if isinstance(value, int) else value[0]
            if not 0 <= byte <= 0xFF:
                raise ValueError("payload values must be in range 0..255")
            if byte == self.END:
                encoded.extend((self.ESC, self.ESC_END))
            elif byte == self.ESC:
                encoded.extend((self.ESC, self.ESC_ESC))
            else:
                encoded.append(byte)

        encoded.append(self.END)
        self._stream.write(encoded)

    # ------------------------------------------------------------------
    # Public RX API
    def poll(self) -> int:
        """Pump the decoder without blocking.

        Returns
        -------
        int
            Number of complete frames now buffered and awaiting :meth:`recv`.
        """

        available = 0
        if hasattr(self._stream, "any"):
            available = self._stream.any() or 0

        if available:
            data = self._stream.read(available)
            if data:
                self.feed(data)
        else:
            # Some streams (like io.BytesIO) do not implement ``any``. Attempt a
            # non-blocking read with ``read`` and fallback to ``feed`` when data
            # arrives.
            if hasattr(self._stream, "read"):
                chunk = self._stream.read()
                if chunk:
                    self.feed(chunk)

        return len(self._rx_frames)

    def recv(self) -> Optional[bytes]:
        """Return the next completed frame or ``None`` when none are queued."""

        if self._rx_frames:
            return self._rx_frames.pop(0)
        return None

    def has_frame(self) -> bool:
        """Whether at least one decoded frame is waiting to be consumed."""

        return bool(self._rx_frames)

    def flush_rx(self) -> None:
        """Drop buffered frames and reset the decoder state."""

        self._rx_frames = []
        self._rx_buffer = bytearray()
        self._escape_pending = False

    def dropped_frames(self) -> int:
        """How many frames have been discarded due to overflow or corruption."""

        return self._dropped_frames

    # ------------------------------------------------------------------
    # Testing helper / alternative entry point
    def feed(self, data: Iterable[int]) -> int:
        """Process raw bytes and return how many frames were decoded."""

        frame_count = 0
        for item in data:
            byte = item if isinstance(item, int) else item[0]
            if not 0 <= byte <= 0xFF:
                continue
            frame_count += self._process_byte(byte)
        return frame_count

    # ------------------------------------------------------------------
    # Decoder internals
    def _process_byte(self, byte: int) -> int:
        if byte == self.END:
            return self._finish_frame()

        if self._escape_pending:
            self._escape_pending = False
            if byte == self.ESC_END:
                return self._append_byte(self.END)
            if byte == self.ESC_ESC:
                return self._append_byte(self.ESC)
            # Invalid escape sequence -> drop frame
            return self._drop_frame()

        if byte == self.ESC:
            self._escape_pending = True
            return 0

        return self._append_byte(byte)

    def _append_byte(self, byte: int) -> int:
        if len(self._rx_buffer) >= self._max_frame_size:
            return self._drop_frame()

        self._rx_buffer.append(byte)
        return 0

    def _finish_frame(self) -> int:
        if not self._rx_buffer:
            # END used as keepalive - ignore
            return 0

        frame = bytes(self._rx_buffer)
        self._rx_frames.append(frame)
        self._rx_buffer = bytearray()
        self._escape_pending = False
        return 1

    def _drop_frame(self) -> int:
        self._rx_buffer = bytearray()
        self._escape_pending = False
        self._dropped_frames += 1
        return 0


__all__ = ["SlipStream"]
